Node.js - tutorial - Node.js events

revision:


the Node.js event loop

The Event Loop is one of the most important aspects to understand about Node.js, because it explains how Node.js can be asynchronous and have non-blocking I/O.

It explains basically the "killer app" of Node.js, the thing that made it this successful.

The Node.js JavaScript code runs on a single thread. There is just one thing happening at a time.

This is a limitation that's actually very helpful, as it simplifies a lot how you program without worrying about concurrency issues.
You just need to pay attention to how you write your code and avoid anything that could block the thread, like synchronous network calls or infinite loops.

In general, in most browsers there is an event loop for every browser tab, to make every process isolated and avoid a web page with infinite loops or heavy processing to block your entire browser.

The environment manages multiple concurrent event loops, to handle API calls for example. Web Workers run in their own event loop as well.

You mainly need to be concerned that your code will run on a single event loop, and write code with this thing in mind to avoid blocking it.

Blocking the event loop

Any JavaScript code that takes too long to return back control to the event loop will block the execution of any JavaScript code in the page, even block the UI thread, and the user cannot click around, scroll the page, and so on.

Almost all the I/O primitives in JavaScript are non-blocking: network requests, filesystem operations, and so on. Being blocking is the exception, and this is why JavaScript is based so much on callbacks, and more recently on promises and async/await.

The call stack

The call stack is a LIFO (Last In, First Out) stack. The event loop continuously checks the call stack to see if there's any function that needs to run. While doing so, it adds any function call it finds to the call stack and executes each one in order.

You know the error stack trace you might be familiar with, in the debugger or in the browser console? The browser looks up the function names in the call stack to inform you which function originates the current call.

The message queue

When setTimeout() is called, the Browser or Node.js starts the timer. Once the timer expires, the callback function is put in the message queue.

The message queue is also where user-initiated events - like click or keyboard events - or fetch responses are queued before your code has the opportunity to react to them. Or also DOM events like onLoad.

The loop gives priority to the call stack, and it first processes everything it finds in the call stack, and once there's nothing in there, it goes to pick up things in the message queue.

We don't have to wait for functions like setTimeout, fetch or other things to do their own work, because they are provided by the browser, and they live on their own threads. For example, if you set the setTimeout timeout to 2 seconds, you don't have to wait 2 seconds - the wait happens elsewhere.

ES6 job queue

ECMAScript 2015 introduced the concept of the job queue, which is used by Promises (also introduced in ES6/ES2015). It's a way to execute the result of an async function as soon as possible, rather than being put at the end of the call stack.

Promises that resolve before the current function ends will be executed right after the current function.


understanding process.nextTick()

One important part of the Node.js event loop is its process.nextTick().

Every time the event loop takes a full trip, we call it a tick.
When we pass a function to process.nextTick(), we instruct the engine to invoke this function at the end of the current operation, before the next event loop tick starts.

example

JS
      copy
      process.nextTick(() => {
        //do something
      })
    

The event loop is busy processing the current function code. When this operation ends, the JS engine runs all the functions passed to nextTick calls during that operation. It's the way we can tell the JS engine to process a function asynchronously (after the current function), but as soon as possible, not queue it.

Calling setTimeout(() => {}, 0) will execute the function at the end of next tick, much later than when using nextTick(), which prioritizes the call and executes it just before the beginning of the next tick.

Use nextTick() when you want to make sure that in the next event loop iteration that code is already executed.


understanding setImmediate()

When you want to execute some piece of code asynchronously, but as soon as possible, one option is to use the setImmediate() function provided by Node.js:

example

JS:
     setImmediate(() => {
      //run something
     })
    

Any function passed as the setImmediate() argument is a callback that's executed in the next iteration of the event loop.

How is setImmediate() different from setTimeout(() => {}, 0) (passing a 0ms timeout), and from process.nextTick()?

A function passed to process.nextTick() is going to be executed on the current iteration of the event loop, after the current operation ends. This means it will always execute before setTimeout and setImmediate.

A setTimeout() callback with a 0ms delay is very similar to setImmediate(). The execution order will depend on various factors, but they will be both run in the next iteration of the event loop.


discover JavaScript timers

setTimeout()

When writing JavaScript code, you might want to delay the execution of a function. This is the job of setTimeout. You specify a callback function to execute later, and a value expressing how later you want it to run, in milliseconds.

example

JS:
      setTimeout(() => {
        // runs after 2 seconds
      }, 2000)

      setTimeout(() => {
        // runs after 50 milliseconds
      }, 50)
    

If you specify the timeout delay to 0, the callback function will be executed as soon as possible, but after the current function execution. This is especially useful to avoid blocking the CPU on intensive tasks and let other functions be executed while performing a heavy calculation, by queuing functions in the scheduler.

setInterval

setInterval is a function similar to setTimeout, with a difference: instead of running the callback function once, it will run it forever, at the specific time interval you specify (in milliseconds).

example

JS:
        const id = setInterval(() => {
          // runs every 2 seconds
        }, 2000)
        
        clearInterval(id)
      

The function above runs every 2 seconds unless you tell it to stop, using clearInterval, passing it the interval id that setInterval returned. It's common to call clearInterval inside the setInterval callback function, to let it auto-determine if it should run again or stop.

recursive setTimeout

setInterval starts a function every n milliseconds, without any consideration about when a function finished its execution. If a function always takes the same amount of time, it's all fine. Maybe the function takes different execution times, depending on network conditions for example. And maybe one long execution overlaps the next one.

To avoid this, you can schedule a recursive setTimeout to be called when the callback function finishes.

example

JS:
      const myFunction = () => {
        // do something

        setTimeout(myFunction, 1000)
      }
      setTimeout(myFunction, 1000)

    

To achieve this scenario, setTimeout and setInterval are available in Node.js, through the Timers module.


JavaScript asynchronous programming and callbacks

JavaScript is synchronous by default and is single threaded. This means that code cannot create new threads and run in parallel. Lines of code are executed in series, one after another.

A callback is a simple function that's passed as a value to another function, and will only be executed when the event happens. We can do this because JavaScript has first-class functions, which can be assigned to variables and passed around to other functions (called higher-order functions). It's common to wrap all your client code in a load event listener on the window object, which runs the callback function only when the page is ready.

Callbacks are used everywhere, not just in DOM events. One common example is by using timers.

How do you handle errors with callbacks? One very common strategy is to use what Node.js adopted: the first parameter in any callback function is the error object: error-first callbacks. If there is no error, the object is null. If there is an error, it contains some description of the error and other information.

example

JS:
      fs.readFile('/file.json', (err, data) => {
        if (err) {
          //handle error
          console.log(err)
          return
        }
      
        //no errors, process data
        console.log(data)
      })
    

Callbacks are great for simple cases! However every callback adds a level of nesting, and when you have lots of callbacks, the code starts to be complicated very quickly.

Alternatives to callbacks: starting with ES6, JavaScript introduced several features that help us with asynchronous code that do not involve using callbacks: Promises (ES6) and Async/Await (ES2017).


understanding JavaScript promises

A promise is commonly defined as a proxy for a value that will eventually become available. Promises are one way to deal with asynchronous code. Promises have been part of the language for years (standardized and introduced in ES2015), and have recently become more integrated, with async and await in ES2017.

Async functions use promises behind the scenes, so understanding how promises work is fundamental to understanding how async and await work.

How promises work

Once a promise has been called, it will start in a pending state. This means that the calling function continues executing, while the promise is pending until it resolves, giving the calling function whatever data was being requested.

The created promise will eventually end in a resolved state, or in a rejected state, calling the respective callback functions (passed to then and catch) upon finishing.

Creating a promise

The Promise API exposes a Promise constructor, which you initialize using new Promise():

example

JS:
        let done = true

        const isItDoneYet = new Promise((resolve, reject) => {
          if (done) {
            const workDone = 'Here is the thing I built'
            resolve(workDone)
          } else {
            const why = 'Still working on something else'
            reject(why)
          }
        })
      

The promise checks the "done" global constant, and if that's true, the promise goes to a "resolved state" (since the resolve callback was called); otherwise, the "reject callback" is executed, putting the promise in a rejected state. (If none of these functions is called in the execution path, the promise will remain in a pending state)

Using resolve and reject, we can communicate back to the caller what the resulting promise state was, and what to do with it. In the above case we just returned a string, but it could be an object, or null as well. Because we've created the promise in the above snippet, it has already started executing.

A more common example you may come across is a technique called Promisifying. This technique is a way to be able to use a classic JavaScript function that takes a callback, and have it return a promise.

Consuming a promise

example

JS:
        const isItDoneYet = new Promise(/* ... as above ... */)
        //...
        
        const checkIfItsDone = () => {
          isItDoneYet
            .then(ok => {
              console.log(ok)
            })
            .catch(err => {
              console.error(err)
            })
        }
      

Running "checkIfItsDone()" will specify functions to execute when the "isItDoneYet" promise "resolves" (in the then call) or "rejects" (in the catch call).

Chaining promises

A promise can be returned to another promise, creating a chain of promises. A great example of chaining promises is the Fetch API, which we can use to get a resource and queue a chain of promises to execute when the resource is fetched. The Fetch API is a promise-based mechanism, and calling fetch() is equivalent to defining our own promise using new Promise().

example

JS
      const status = response => {
        if (response.status >= 200 && response.status < 300) {
          return Promise.resolve(response)
        }
        return Promise.reject(new Error(response.statusText))
      }
     
      const json = response => response.json()
      
      fetch('/todos.json')
        .then(status)    // note that the `status` function is actually **called** here, and that it **returns a promise***
        .then(json)      // likewise, the only difference here is that the `json` function here returns a promise that resolves with `data`
        .then(data => {  // ... which is why `data` shows up here as the first parameter to the anonymous function
          console.log('Request succeeded with JSON response', data)
        })
        .catch(error => {
          console.log('Request failed', error)
        })
    

In this example, we call fetch() to get a list of TODO items from the todos.json file found in the domain root, and we create a chain of promises. Running fetch() returns a "response", which has many properties, and within those we reference:
- status, a numeric value representing the HTTP status code;
- statusText, a status message, which is OK if the request succeeded

"response" also has a json() method, which returns a promise that will resolve with the content of the body processed and transformed into JSON.

So given those promises, this is what happens: the first promise in the chain is a function that we defined, called status(), that checks the response status and if it's not a success response (between 200 and 299), it rejects the promise. This operation will cause the promise chain to skip all the chained promises listed and will skip directly to the catch() statement at the bottom, logging the Request failed text along with the error message. If that succeeds instead, it calls the json() function we defined. Since the previous promise, when successful, returned the response object, we get it as an input to the second promise. In this case, we return the data JSON processed, so the third promise receives the JSON directly and we simply log it to the console.

Handling errors

When anything in the chain of promises fails and raises an error or rejects the promise, the control goes to the nearest catch() statement down the chain.

If inside the catch() you raise an error, you can append a second catch() to handle it, and so on.

Orchestrating promises

Promise.all(): if you need to synchronize different promises, Promise.all() helps you define a list of promises, and execute something when they are all resolved.

Promise.race() runs when the first of the promises you pass to it settles (resolves or rejects), and it runs the attached callback just once, with the result of the first promise settled.


modern asynchronous JavaScript with async and await

An async function returns a promise, like in this example:

example

JS
      const doSomethingAsync = () => {
        return new Promise(resolve => {
          setTimeout(() => resolve('I did something'), 3000)
        })
      }
    

When you want to call this function you prepend await, and the calling code will stop until the promise is resolved or rejected. One caveat: the client function must be defined as async. Here's an example:

example

JS
      const doSomething = async () => {
        console.log(await doSomethingAsync())
      }
    

Prepending the async keyword to any function means that the function will return a promise. Even if it's not doing so explicitly, it will internally make it return a promise.

Async functions can be chained very easily, and the syntax is much more readable than with plain promises.

Debugging promises is hard because the debugger will not step over asynchronous code. Async/await makes this very easy because to the compiler it's just like synchronous code.


The Node.js event emitter

If you worked with JavaScript in the browser, you know how much of the interaction of the user is handled through events: mouse clicks, keyboard button presses, reacting to mouse movements, and so on.

On the backend side, Node.js offers us the option to build a similar system using the events module. This module, in particular, offers the EventEmitter class, which we'll use to handle our events. You initialize that using:

JS:
      const EventEmitter = require('events')
      const eventEmitter = new EventEmitter()
    

This object exposes, among many others, the on and emit methods: "emit" is used to trigger an event; "on" is used to add a callback function that's going to be executed when the event is triggered.

example

JS
    eventEmitter.on('start', () => {
      console.log('started')
    })
    
when we run: JS:
      eventEmitter.emit('start')
    

The event handler function is triggered, and we get the console log.

You can pass arguments to the event handler by passing them as additional arguments to emit().

example

JS
      eventEmitter.on('start', (start, end) => {
        console.log(`started from ${start} to ${end}`)
      })
      
      eventEmitter.emit('start', 1, 100)
    

The EventEmitter object also exposes several other methods to interact with events, like:
- once(): add a one-time listener;
- removeListener() / off(): remove an event listener from an event;
- removeAllListeners(): remove all listeners for an event